package com.example.lotterysystem.service.mq;

import cn.hutool.core.date.DateUtil;
import com.example.lotterysystem.common.exception.ServiceException;
import com.example.lotterysystem.common.util.JacksonUtil;
import com.example.lotterysystem.common.util.MailUtil;
import com.example.lotterysystem.common.util.SMSUtil;
import com.example.lotterysystem.controller.param.DrawPrizeParam;
import com.example.lotterysystem.dao.dataObject.ActivityPrizeDO;
import com.example.lotterysystem.dao.dataObject.WinnerRecordDO;
import com.example.lotterysystem.dao.mapper.ActivityPrizeMapper;
import com.example.lotterysystem.dao.mapper.WinningRecordMapper;
import com.example.lotterysystem.service.DrawPrizeService;
import com.example.lotterysystem.service.activitystatus.ActivityStatusManager;
import com.example.lotterysystem.service.dto.ConvertActivityStatusDTO;
import com.example.lotterysystem.service.enums.ActivityPrizeStatusEnum;
import com.example.lotterysystem.service.enums.ActivityPrizeTiersEnum;
import com.example.lotterysystem.service.enums.ActivityStatusEnum;
import com.example.lotterysystem.service.enums.ActivityUserStatusEnum;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

import static com.example.lotterysystem.common.config.DirectRabbitConfig.QUEUE_NAME;

@Component
@RabbitListener(queues = QUEUE_NAME) //监听器,这里监听的是队列
public class MqReceiver {
    //用来接受消息
    private static final Logger logger = LoggerFactory.getLogger(MqReceiver.class);

    @Autowired
    private DrawPrizeService drawPrizeService;
    @Autowired
    private ActivityStatusManager activityStatusManager;
    @Autowired
    private ThreadPoolTaskExecutor threadPoolTaskExecutor;
    @Autowired
    private MailUtil mailUtil;
    @Autowired
    private SMSUtil smsUtil;
    @Autowired
    private ActivityPrizeMapper activityPrizeMapper;
    @Autowired
    private WinningRecordMapper winningRecordMapper;

    @RabbitHandler //专门用来处理消息的方法的注解,传的是什么消息就会接受什么类型的消息
    public void process(Map<String,String> message){
        //成功接受到队列中的消息
        logger.info("MQ成功接收到消息,message:{}",
                JacksonUtil.writeValueAsString(message));
        String paramString = message.get("messageData");
        DrawPrizeParam param = JacksonUtil.readValue(paramString, DrawPrizeParam.class);//进行反序列化
        //处理抽奖的流程

        //以下需要进行异常抛出,才能使MQ知道需要进行回滚
        try {
            //校验抽奖请求是否有效
            //1.可能前端发起两个一样的抽奖请求,对于param来说也是一样的两个请求
            //2.param:同一个活动下的最后一个奖项,相当于两个人抢一个岗位,会出现问题
            //也就是说消费者会消费两次,但是第一个不会有问题,会将所有信息扭转,但是第二个还没消费,就无法校验了(活动已经有效结束了)
            //此时就会进行回滚,又会变成没抽完的状态
            if(!drawPrizeService.checkDrawPrizeParam(param)){
                return;
            }

            //活动、奖品、人员状态处理(状态扭转)
            statusConvert(param);

            //保存中奖者名单
            List<WinnerRecordDO> winnerRecordDOList =  drawPrizeService.saveWinnerRecords(param);

            //通知中奖者(短信、邮箱) 根据中奖信息来发短信 --> 异步并发
            // 抽奖之后的后续流程
            syncExecute(winnerRecordDOList);


        }catch (ServiceException e){
            logger.error("处理MQ消息异常! {}:{}",e.getCode(),e.getMessage(),e);
            //回滚,保证事务一致性
            rollback(param);
            // 抛出异常 -->消息重试(解决异常:代码、网络、服务器...)
            throw e;
        }catch (Exception e){
            logger.error("处理MQ消息异常! {}",e.getMessage(),e);
            //回滚,保证事务一致性,抛出异常
            rollback(param);
            throw e;
        }
    }

    /**
     * 处理抽奖异常的回滚行为 恢复处理请求之前的库表状态
     * @param param
     */
    private void rollback(DrawPrizeParam param) {
        //需要回滚的是什么?
        //对已经被改变的数据进行恢复操作,同时这个恢复的顺序是一定的
        //定位异常出现的位置
        //1.回滚状态:活动、奖品、人员
        //判断状态是否需要回滚(因为状态是有一个本地备份的)
        //数据库修改了但是不是代表当前的DrawPrizeParam被修改了
        if(!statusNeedRollback(param)){
            //不需要回滚: return --> 因为最开始就是先处理的状态,如果状态都没有成功操作,后续的操作也不会成功
            return;
        }

        //需要回滚
        rollbackStatus(param);

        //2.中奖者名单
        //是否需要回滚
        if(!winnerNeedRollback(param)){
            //不需要:return
            return;
        }

        //需要
        rollbackWinner(param);
    }

    /**
     * 回滚中奖记录,删除奖品下的一些中奖者
     * @param param
     */
    private void rollbackWinner(DrawPrizeParam param) {
        drawPrizeService.deleteRecords(param.getActivityId(), param.getPrizeId());
    }

    private boolean winnerNeedRollback(DrawPrizeParam param) {
        //通过奖品是否有中奖者就能判断出是否已经生成了中奖者名单
        int count = winningRecordMapper.countByAPId(param.getActivityId(),param.getPrizeId());
        return count>0;
        //>0 表示有中奖者生成,需要进行回滚
    }

    /**
     * 恢复相关状态的方法
     * @param param
     */
    private void rollbackStatus(DrawPrizeParam param) {
        // 设计状态的恢复工作: ActivityStatusManager
        ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();//需要回滚成的状态
        convertActivityStatusDTO.setActivityId(param.getActivityId());
        convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.RUNNING);
        convertActivityStatusDTO.setPrizeId(param.getPrizeId());
        convertActivityStatusDTO.setTargetActivityPrizeStatus(ActivityPrizeStatusEnum.INIT);
        convertActivityStatusDTO.setUserIds(param.getWinnerList()
                .stream()
                .map(DrawPrizeParam.Winner::getUserId)
                .collect(Collectors.toList()));
        convertActivityStatusDTO.setTargetActivityUserStatus(ActivityUserStatusEnum.INIT);

        activityStatusManager.rollbackHandlerEvent(convertActivityStatusDTO);
    }

    private boolean statusNeedRollback(DrawPrizeParam param) {
        // 判断活动+奖品+活动关联人员 的status是否已经被扭转(正常思路)
        //扭转时保证了事务的一致性(注解),要不就是都扭转了,要不就是都没有扭转(但是不包含活动-->所有抽完才会扭转)
        //只需要判断人员或者奖品是否扭转过就可以知道status是否有扭转 --> 不能用活动来进行判断
        //结论:选择一个进行判断就行
        ActivityPrizeDO activityPrizeDO = activityPrizeMapper.selectByAPId(param.getActivityId(), param.getPrizeId());
        return activityPrizeDO.getStatus().equalsIgnoreCase(ActivityPrizeStatusEnum.COMPLETED.name());
        //true --> 需要回滚   false --> 不需要
    }

    /**
     * 并发处理抽奖后续流程
     * @param winnerRecordDOList
     */
    private void syncExecute(List<WinnerRecordDO> winnerRecordDOList) {
        //通过线程池来处理后续任务
        //扩展: 加入策略模式或者其他设计模式来完成后续的异步操作
        //短信通知
        threadPoolTaskExecutor.execute(()->sendMessage(winnerRecordDOList));

        //邮件通知
        threadPoolTaskExecutor.execute(()->sendMail(winnerRecordDOList));


    }

    /**
     * 发邮件
     * @param winnerRecordDOList
     */
    private void sendMail(List<WinnerRecordDO> winnerRecordDOList) {
        if(CollectionUtils.isEmpty(winnerRecordDOList)){
            logger.info("中奖列表为空,不用发邮件!");
            return;
        }
        for (WinnerRecordDO winnerRecordDO:winnerRecordDOList){
            String context = "Hi! "+winnerRecordDO.getWinnerName()+",恭喜你在"+winnerRecordDO.getActivityName()+"中获得了"+
                    ActivityPrizeTiersEnum.forName(winnerRecordDO.getPrizeTier()).getMsg()+": "+
                    winnerRecordDO.getPrizeName()+"。 获奖时间为: "+winnerRecordDO.getWinningTime()+
                    "请尽快前往网页领取您的奖励!";
            mailUtil.sendSampleMail(winnerRecordDO.getWinnerEmail(),
                    "中奖通知",context);
        }
    }

    /**
     * 发短信
     * @param winnerRecordDOList
     */
    private void sendMessage(List<WinnerRecordDO> winnerRecordDOList) {
        if(CollectionUtils.isEmpty(winnerRecordDOList)){
            logger.info("中奖列表为空,不用发短信!");
            return;
        }
        for (WinnerRecordDO winnerRecordDO:winnerRecordDOList){
            Map<String,String> map = new HashMap<>();
            //占位符
            map.put("name",winnerRecordDO.getWinnerName());
            map.put("activityName",winnerRecordDO.getActivityName());
            map.put("prizeTiers",ActivityPrizeTiersEnum.forName(winnerRecordDO.getPrizeTier()).getMsg());
            map.put("prizeName",winnerRecordDO.getPrizeName());
            map.put("winningTime", DateUtil.formatTime(winnerRecordDO.getWinningTime()));
            smsUtil.sendMessage("SMS_478465702",winnerRecordDO.getWinnerPhoneNumber().getValue(),
                    JacksonUtil.writeValueAsString(map));
        }
    }


    /**
     * 状态扭转
     * @param param
     */
    private void statusConvert(DrawPrizeParam param){
        ConvertActivityStatusDTO convertActivityStatusDTO = new ConvertActivityStatusDTO();
        convertActivityStatusDTO.setActivityId(param.getActivityId());
        convertActivityStatusDTO.setTargetActivityStatus(ActivityStatusEnum.COMPLETED);
        convertActivityStatusDTO.setPrizeId(param.getPrizeId());
        convertActivityStatusDTO.setTargetActivityPrizeStatus(ActivityPrizeStatusEnum.COMPLETED);
        convertActivityStatusDTO.setUserIds(param.getWinnerList()
                .stream()
                .map(DrawPrizeParam.Winner::getUserId)
                .toList());
        convertActivityStatusDTO.setTargetActivityUserStatus(ActivityUserStatusEnum.COMPLETED);


        activityStatusManager.handlerEvent(convertActivityStatusDTO);
    }


//    private void statusConvert(DrawPrizeParam param) {
//
//        //问题:
//        //1.活动状态扭转有依赖性
//        //2.状态扭转的条件可能会扩展,那么当前的写法维护性就会很差(当产生新的依赖时,活动扭转就必须要修改写法)
//        //3.代码灵活性、拓展性和维护性差
//        //解决: 设计模式(责任链设计模式、策略模式)
//
//        //活动: RUNNING -> COMPLETED 全部奖品都被抽完之后才会改变状态
//        //奖品 INIT-> COMPLETED
//        //人员列表: INIT-> COMPLETED
//
//        //1.扭转奖品状态
//        //查询活动关联的奖品信息
//        //条件判断是否符合扭转奖品状态: 判断当前状态是否是 COMPLETED,如果是就不需要进行扭转
//
//        //2.扭转人员状态
//        //查询活动关联的人员信息
//        //条件判断是否符合扭转人员状态: 判断当前状态是否不是 COMPLETED ,不是就要扭转
//        //是,才会进行扭转
//
//        //3.扭转活动状态(必须在扭转奖品状态之后完成)
//        //查询活动信息
//        //条件判断是否符合扭转人员状态: 判断当前状态是否不是 COMPLETED ,如果不是并且全部奖品都抽完之后才会改变状态
//        //是,才会进行扭转
//
//        //更新缓存,将活动完整信息更新
//    }
}
